# -*- coding: utf-8 -*-
import argparse
import os.path
import sys

import pymysql.cursors
from keystoneauth1 import identity
from keystoneauth1 import session
from neutronclient.v2_0 import client

ALL_MODULES = (
    'analytics', 'apm', 'asm', 'auth', 'cli', 'cm', 'gtm', 'ilx', 'ltm', 'mgmt', 'net', 'pem', 'security', 'sys',
    'util', 'vcmp', 'wam', 'wom')

"""
1. 新加入的BIG-IP有配置残留，此时merge配置会如何？
    
   经过测试，merge的配置会覆盖残留配置。
2. self ip不仅需要手动修改merge文件，neutron数据库的ports表也需要添加self ip记录。

   手动操作步骤：

   1）`neutron port-show local-b_device_name-subnetid`查看b上的self ip的port创建信息。如果能查到，则不需再创建，从查到的信息中把ip
   获取到，更新到merge文件。若查不到，则需要执行第二步，手动添加port信息。

   2）`neutron port-show local-a_device_name-subnetid`查看a上的self ip的创建信息，在创建b的self ip port时需要除了name字段，其他都要和a相同。
   执行`neutron port-create network-id --name local-b_device_name-subnetid`创建b的self ip port，从返回的信息中获取到ip，填入merge文件。

   以上这两部可以考虑程序实现。
   1)从merge文件中解析出a设备的self ip的name，拼接出b的name，调用neutron client根据port-name查询b。若存在则获取其ip，复制到merge文件。若不存在，执行第二步。
   
   2)根据a的self ip name调neutron client port api获取a的详细信息，根据a的详细信息封装create b port的数据，除了name其他都可以相同。
   然后调用api创建b port，从创建成功的返回值中获取ip，将其替换到merge文件。


编辑scf-filename-merge文件，需要修改所有self ip的name和address。因为HA的两台BIG-IP的lbaas配置中，除了self ip的配置有差异，其他配置都相同。

如下为scf中self-ip的配置示例
net self /CORE_d2e28c1b93604c9eb8af6f486b3cb689/local-bigip1-0e9f9b23-368b-4999-818d-a42ee6bde9f5 {
   address 10.251.36.12%936/24
   traffic-group /Common/traffic-group-local-only
   vlan /CORE_d2e28c1b93604c9eb8af6f486b3cb689/vlan-936
}
1. 第一行中bigip1为配置的HA device_group中老BIG-IP的name，需要修改为新加入BIG-IP的name。
2. 第二行把address中的ip`10.251.36.12`修改为同VLAN中一个尚未使用的ip。

修改后的配置示例如下
net self /CORE_d2e28c1b93604c9eb8af6f486b3cb689/local-bigip2-0e9f9b23-368b-4999-818d-a42ee6bde9f5 {
   address 10.251.36.14%936/24
   traffic-group /Common/traffic-group-local-only
   vlan /CORE_d2e28c1b93604c9eb8af6f486b3cb689/vlan-936
}

"""


class ScfParser:
    def __init__(self, filepath=None):
        self.filepath = filepath
        self.data_list = []
        self.out_list = []
        self.length = 0
        # modules need to be merged
        self.merge_modules = ('ltm', 'net', 'sys file', 'sys folder', 'auth partition', 'cli')
        self.except_modules = ('net self /Common', 'net arp', 'net fdb', 'net tunnels', 'cm',
                               'sys file external-monitor', 'sys file ssl-cert /Common/f5_api_com.crt',
                               'sys file ssl-key /Common/f5_api_com.key', 'sys folder /Common',
                               'sys folder / {', 'ltm data-group')
        self.acl_prefix = 'ltm data-group internal /Common/acl'

    def parse(self):
        with open(self.filepath) as f:
            for line in f:
                self.data_list.append(line)

    def extract(self):
        n = len(self.data_list)
        i = 0
        while i < n:
            if (self.data_list[i].startswith(self.merge_modules) and
                not self.data_list[i].startswith(self.except_modules)) \
                    or self.data_list[i].startswith(self.acl_prefix):
                # one line config
                if self.data_list[i].endswith("}"):
                    self.out_list.append(self.data_list[i])
                else:
                    # multi line config
                    while i < n:
                        line = self.data_list[i]
                        if 'creation-time' in line or 'last-modified-time' in line:
                            i += 1
                            continue

                        self.out_list.append(line)
                        # 有可能连着两行开头都为 '}'，以最后一个'}'为结尾
                        if i == n - 1:
                            line.startswith("}")
                            break
                        else:
                            if line.startswith("}") and self.data_list[i + 1].startswith(ALL_MODULES):
                                break
                        i += 1
            i += 1

    def output(self):
        out_filename = os.path.basename(self.filepath) + '-merge'
        out_path = os.path.join(os.path.dirname(self.filepath), out_filename)

        with open(out_path, mode='w') as f:
            for line in self.out_list:
                f.write(line)
        print('scf merge finished, new scf was saved in path: {}'.format(out_path))
        return out_path

    def run(self):
        self.parse()
        self.extract()
        out_path = self.output()
        return out_path


# def connect_neutron_db(db_conn):
#     db_type = db_conn[0:db_conn.find("+")]
#     if db_type != "mysql":
#         print("Only support MySQL DB")
#         sys.exit(1)
#
#     u_start = db_conn.find("//") + 2
#     u_end = db_conn.find(":", u_start)
#     p_start = u_end + 1
#     p_end = db_conn.find("@", p_start)
#     h_start = p_end + 1
#     h_end = db_conn.find("/", h_start)
#     d_start = h_end + 1
#
#     return pymysql.connect(host=db_conn[h_start:h_end],
#                            user=db_conn[u_start:u_end],
#                            password=db_conn[p_start:p_end],
#                            database=db_conn[d_start:],
#                            cursorclass=pymysql.cursors.DictCursor)


class SelfProcess:
    def __init__(self, filepath=None, standalone_device_name=None, rma_device_name=None, conn=None, region_name=None):
        self.filepath = filepath
        self.standalone_device_name = standalone_device_name
        self.rma_device_name = rma_device_name
        self.conn = conn
        self.data_list = []
        self.region_name = region_name
        self.neutron = self.get_neutron_client()

    def get_neutron_client(self):
        auth_url = os.environ['OS_AUTH_URL']
        username = os.environ['OS_USERNAME']
        password = os.environ['OS_PASSWORD']
        project_name = os.environ['OS_PROJECT_NAME']
        project_domain_id = 'default'
        user_domain_id = 'default'
        print(auth_url, username, password, project_name)
        auth = identity.Password(auth_url=auth_url,
                                 username=username,
                                 password=password,
                                 project_name=project_name,
                                 project_domain_id=project_domain_id,
                                 user_domain_id=user_domain_id)
        sess = session.Session(auth=auth)
        if self.region_name:
            neutron = client.Client(session=sess, region_name=self.region_name)
        else:
            neutron = client.Client(session=sess)
        return neutron

    def parse(self):
        with open(self.filepath) as f:
            for line in f:
                self.data_list.append(line)

    def output(self):
        with open(self.filepath, 'w') as f:
            for line in self.data_list:
                f.write(line)
        print('self ip reconfig finished')

    def replace_self_name(self, standalone_self_line):
        """
        替换net self行的device name
        :param line:
        :return:
        """
        print('before replace standalone_line: {}'.format(standalone_self_line))
        standalone_self_name = standalone_self_line.replace('{', '').strip().split('/')[-1]
        print('standalone self name: {}'.format(standalone_self_name))
        rma_self_line = standalone_self_line.replace(self.standalone_device_name, self.rma_device_name)
        print('after replace, new_line: {}'.format(rma_self_line))
        return rma_self_line, standalone_self_name

    # def set_port_status_active(self, rma_port_id):
    #     with self.conn:
    #         with self.conn.cursor() as cursor:
    #             sql = """
    #             UPDATE ports SET status=%s WHERE id=%s
    #             """
    #             cursor.execute(sql, ("ACTIVE", rma_port_id))

    def get_new_sefip(self, standalone_self_name, standalone_address_line):
        """
        处理net self中的 address行，替换ip address。
        从数据库查看是否已经存在rma port，若存在，获取其ip；若不存在，创建port后获取其ip。
        :param standalone_self_name:
        :param standalone_address_line:
        :return:
        """
        print('standalone self name: {}'.format(standalone_self_name))
        ports = self.neutron.list_ports(name=standalone_self_name)
        standalone_port = ports['ports'][0]
        print('standalone port detail:')
        print(standalone_port)

        standalone_subnet_id = standalone_port['fixed_ips'][0]['subnet_id']
        standalone_ip_address = standalone_port['fixed_ips'][0]['ip_address']
        print('standalone subnet_id: {}, ip_address: {}'.format(standalone_subnet_id, standalone_ip_address))

        rma_self_name = standalone_self_name.replace(self.standalone_device_name, self.rma_device_name)
        print('rma self name: {}'.format(rma_self_name))

        rma_ports = self.neutron.list_ports(name=rma_self_name)
        print('get ports by rma_self_name, ports: {}'.format(rma_ports))
        if len(rma_ports['ports']) == 0:
            # 创建port
            create_new_port_body = {
                'admin_state_up': True,
                'binding:host_id': standalone_port['binding:host_id'],
                'binding:profile': standalone_port['binding:profile'],
                'binding:vnic_type': standalone_port['binding:vnic_type'],
                'device_id': standalone_port['device_id'],
                'device_owner': standalone_port['device_owner'],
                'fixed_ips': [{'subnet_id': standalone_subnet_id}],
                'name': rma_self_name,
                'network_id': standalone_port['network_id'],
                'port_security_enabled': standalone_port['port_security_enabled'],
                'project_id': standalone_port['project_id'],
                'tenant_id': standalone_port['tenant_id'],
            }
            self.neutron.create_port({'port': create_new_port_body})
            rma_ports = self.neutron.list_ports(name=rma_self_name)
            rma_port_id = rma_ports['ports'][0]['id']
            rma_ip_address = rma_ports['ports'][0]['fixed_ips'][0]['ip_address']
            print('create new rma port, ip address is: {}'.format(rma_ip_address))

            # neutron cli创建完port后其status为DOWN
            # 但是api不允许设置port status,neutronclient.common.exceptions.BadRequest: Cannot update read-only attribute status
            # updated_rma_ports = self.neutron.update_port(rma_port_id, {'port': {'status': 'ACTIVE'}})
            # print('after update rma ports: {}'.format(updated_rma_ports))
            # rma_ip_address = updated_rma_ports['ports'][0]['fixed_ips'][0]['ip_address']
            # print('create new rma port, ip address is: {}'.format(rma_ip_address))

            # 现网环境不能直连数据库也不需修改。
            # self.set_port_status_active(rma_port_id)
            # print('update port {} set status ACTIVE'.format(rma_port_id))
        else:
            rma_ip_address = rma_ports['ports'][0]['fixed_ips'][0]['ip_address']
            print('rma address already exists: {}'.format(rma_ip_address))

        print('standalone_address_line: {}'.format(standalone_address_line))
        rma_address_line = standalone_address_line.replace(standalone_ip_address, rma_ip_address)
        print('rma address line: {}'.format(rma_address_line))
        return rma_address_line

    def process(self):
        n = len(self.data_list)
        i = 0
        self_prefix = 'net self /'
        while i < n:
            line = self.data_list[i]
            if line.startswith(self_prefix):
                self.data_list[i], standalone_self_name = self.replace_self_name(line)
                while i < n and not self.data_list[i].startswith('}'):
                    line = self.data_list[i]
                    if line.strip().startswith('address'):
                        rma_address_line = self.get_new_sefip(standalone_self_name, line)
                        self.data_list[i] = rma_address_line
                        break
                    i += 1
            i += 1

    def run(self):
        self.parse()
        self.process()
        self.output()


if __name__ == '__main__':
    parser = argparse.ArgumentParser(usage="python merge_scf.py --scf-path='/local/path/scf-filename'")
    parser.add_argument('--scf-path', type=str, help='your local scf absolute path', required=True)
    parser.add_argument('--standalone-device-name', type=str, help='device name of standalone BIG-IP', required=True)
    parser.add_argument('--rma-device-name', type=str, help='device name of RMA BIG-IP ', required=True)
    # parser.add_argument("--db-connection", type=str, help="Neutron DB connection string", required=True)
    parser.add_argument('--region-name', type=str, help='region name of neutron client used ')
    args = parser.parse_args()
    scf = ScfParser(filepath=args.scf_path)
    out_path = scf.run()

    # conn = connect_neutron_db(args.db_connection)
    sp = SelfProcess(out_path, standalone_device_name=args.standalone_device_name, rma_device_name=args.rma_device_name, region_name=args.region_name)
    sp.run()

"""
python merge_scf.py --scf-path='./files/10.250.11.193-194/193' --standalone-device-name=bigip2 --rma-device-name=bigip1.standby.self
"""

"""
# 备份系统配置
save sys config file <backup-filename> no-passphrase 
# 如果出现问题，使用备份配置恢复系统
load sys config file <backp-filename>
"""
